﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using Newtonsoft.Json;
using FBGraph.Configuration;

namespace FBGraph.Async
{
    /// <summary>Encapsulates asynchronous functionality used by <see cref="IFBGraphContext" /> implementations.</summary>
    public class AsyncContainer : IDisposable
    {
        /// <summary>Occurs when the <see cref="AsyncContainer" /> is disposed.</summary>
        public event EventHandler Disposed;

        /// <summary>A container for data about errors that occur during asynchronous execution.</summary>
        public class AsyncErrorData
        {
            internal AsyncErrorData(Uri uri, Exception ex)
            {
                this.Exception = ex;
                this.Uri = uri;
            }

            /// <summary>Gets the exception that was thrown.</summary>
            public Exception Exception { get; private set; }

            /// <summary>Gets the <see cref="Uri" /> that was being called when the error occurred.</summary>
            public Uri Uri { get; private set; }
        }

        private delegate void CompleteCaller();

        private class CompleteState
        {
            public Object UserState { get; set; }
            public AsyncCallback UserCallback { get; set; }
            public CompleteCaller Caller { get; set; }
        }

        private IFBGraphContext context;
        private ManualResetEvent mre;
        internal Queue<IFBGraphAsyncRequest> InnerQueue { get; private set; }

        internal AsyncContainer(IFBGraphContext context)
        {
            this.context = context;
            this.InnerQueue = new Queue<IFBGraphAsyncRequest>();
            this.errors = new List<AsyncErrorData>();
        }

        internal void Enqueue(IFBGraphAsyncRequest item)
        {
            item.Error += new FBGraphAsyncErrorHandler(item_Error);

            this.InnerQueue.Enqueue(item);
            this.Execute();
        }

        void item_Error(object sender, FBGraphAsyncErrorEventArgs e)
        {
            this.errors.Add(new AsyncErrorData(e.Uri, e.Exception));
        }

        private List<AsyncErrorData> errors;
        /// <summary>Gets a collection of errors that occurred during the execution of the requests queued in the <see cref="AsyncContainer" />.</summary>
        public ICollection<AsyncErrorData> Errors { get { return errors; } }

        private Boolean isExecuting;
        private Int32 waiting;
        private void Execute()
        {
            if (isExecuting) return;
            else
            {
                isExecuting = true;

                if (this.InnerQueue.Count > 0)
                {
                    if (mre == null) mre = new ManualResetEvent(false);
                    else mre.Reset();
                }

                while (this.InnerQueue.Count > 0)
                {
                    var item = this.InnerQueue.Dequeue(); // get the next item

                    // if the item has an uncompleted dependency, re-enqueue it
                    if (item.Dependency != null && !item.Dependency.IsCompleted)
                    {
                        this.InnerQueue.Enqueue(item);
                        if (this.InnerQueue.Count == 1) Thread.Sleep(0);    // if this item is the only item left,
                        continue;                                           // suspend the thread to let other threads continue
                    }

                    item.Attempts++;
                    item.InnerRequest.BeginGetResponse(new AsyncCallback(GetResponseCallback), item);
                    waiting++;
                }

                isExecuting = false;
            }
        }

        private void GetResponseCallback(IAsyncResult result)
        {
            var item = (IFBGraphAsyncRequest)result.AsyncState;
            try
            {
                using (var response = (HttpWebResponse)item.InnerRequest.EndGetResponse(result))
                {
                    String content = null;
                    using (var reader = new StreamReader(response.GetResponseStream()))
                    {
                        content = reader.ReadToEnd();
                    }
                    item.Result.UpdateFrom((IFBGraphResult)JsonConvert.DeserializeObject(content, item.ReturnType));
                }
            }
            catch (WebException ex)
            {
                var config = new FBGraphConfigurationManager();
                var apiConfig = config.GetApiConfiguration();
                var hasRetries = apiConfig.RetryCount - item.Attempts >= 0;

                if (hasRetries && ex.Status == WebExceptionStatus.Timeout)
                {
                    this.Enqueue(item);
                    this.Execute();
                    return;
                }

                FBGraphApiException fgex;
                if (FBGraphApiHelper.HandleApiError(ex, out fgex))
                {
                    if (hasRetries && fgex.Type == null)
                    {
                        this.Enqueue(item);
                        this.Execute();
                        return;
                    }

                    item.Exception = fgex;
                    item.OnError(new FBGraphAsyncErrorEventArgs(item.InnerRequest.RequestUri, fgex));
                }
                else
                {
                    item.Exception = ex;
                    item.OnError(new FBGraphAsyncErrorEventArgs(item.InnerRequest.RequestUri, ex));
                }
            }
            catch (Exception ex)
            {
                item.Exception = ex;
                item.OnError(new FBGraphAsyncErrorEventArgs(item.InnerRequest.RequestUri, ex));
            }
            finally
            {
                item.IsCompleted = true;
                waiting--;
                if (waiting == 0) mre.Set();
            }
        }

        private void Complete()
        {
            AsyncContainer.Complete(this);
        }

        internal static void Complete(AsyncContainer container)
        {
            container.mre.WaitOne();

            if (container.Errors.Any())
            {
                throw new FBGraphAsyncApiException(container.Errors);
            }
        }

        internal IAsyncResult BeginComplete(AsyncCallback callback, Object state)
        {
            var userCompleteState = new CompleteState
            {
                Caller = new CompleteCaller(Complete),
                UserCallback = callback,
                UserState = state
            };

            return userCompleteState.Caller.BeginInvoke(EndComplete, userCompleteState);
        }

        internal void EndComplete(IAsyncResult result)
        {
            var userCompleteState = (CompleteState)result.AsyncState;
            userCompleteState.Caller.EndInvoke(result);
            userCompleteState.UserCallback.Invoke(result);
        }

        #region [ IDisposable Members ]

        void IDisposable.Dispose()
        {
            this.InnerQueue.Clear();
            if (mre != null) ((IDisposable)mre).Dispose();

            this.OnDisposed(EventArgs.Empty);
        }

        #endregion

        /// <summary>Raises the <see cref="Disposed" /> event.</summary>
        /// <param name="e">A <see cref="EventArgs" /> object.</param>
        protected virtual void OnDisposed(EventArgs e)
        {
            if (this.Disposed != null) this.Disposed(this, e);
        }
    }
}
